package com.egg.basic.controller.web;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.Page;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.TemplateConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.egg.common.core.constant.StringPool;
import com.egg.common.core.exception.CustomException;
import com.egg.common.core.model.controller.WebController;
import com.egg.common.core.model.entity.BaseEntity;
import com.egg.common.core.model.response.PageData;
import com.egg.common.core.model.response.ResponseData;
import com.egg.common.mybatis.service.IBaseService;
import com.egg.common.mybatis.service.impl.BaseServiceImpl;
import com.zaxxer.hikari.HikariDataSource;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Slf4j
@Api(tags = WebController.API_PREFIX + "代码生成器")
@RestController
@RequestMapping("/code/generator")
public class WebCodeGeneratorController extends WebController {


    @Autowired
    private DataSourceConfig dataSourceConfig;


    @Bean
    public DataSourceConfig dataSourceConfig(
            @Value("${spring.shardingsphere.datasource.master0.driver-class-name}") String driverName
            , @Value("${spring.shardingsphere.datasource.master0.url}") String url
            , @Value("${spring.shardingsphere.datasource.master0.username}") String username
            , @Value("${spring.shardingsphere.datasource.master0.password}") String password
    ) {
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        dataSourceConfig.setDriverName(driverName)
                .setUrl(url)
                .setUsername(username)
                .setPassword(password);
        return dataSourceConfig;
    }


    private final String TABLE_SQL_TEMPLATE = "select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables where table_schema = (select database()) %s order by create_time desc";

    private final String PAGE_SQL_TEMPLATE = " limit ?,?";

    private final String COUNT_SQL_TEMPLATE = "select count(1) from (%s)tmp";


    @ApiOperation(value = "表格选择")
    @GetMapping("/table")
    public ResponseData<PageData<Entity>> listTables(
            @ApiParam(value = "页数", required = true) @RequestParam(value = "current", defaultValue = "1") Integer current
            , @ApiParam(value = "页长", required = true) @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize
            , @ApiParam(value = "数据库地址") @RequestParam(value = "dbUrl", defaultValue = "jdbc:mysql://127.0.0.1:3306/egg", required = false) String dbUrl
            , @ApiParam(value = "数据库用户名") @RequestParam(value = "dbUsername", defaultValue = "root", required = false) String dbUsername
            , @ApiParam(value = "数据库密码") @RequestParam(value = "dbPassword", defaultValue = "123456", required = false) String dbPassword
            , @ApiParam(value = "表名") @RequestParam(value = "tableName", required = false) String tableName
    ) throws SQLException {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(ObjectUtil.isNotEmpty(dbUrl) ? dbUrl : dataSourceConfig.getUrl());
        dataSource.setUsername(ObjectUtil.isNotEmpty(dbUsername) ? dbUsername : dataSourceConfig.getUsername());
        dataSource.setPassword(ObjectUtil.isNotEmpty(dbPassword) ? dbPassword : dataSourceConfig.getPassword());
        Db db = new Db(dataSource);
        Page page = new Page(current - 1, pageSize);
        String paramSql = "";
        if (ObjectUtil.isNotEmpty(tableName)) {
            paramSql = "and table_name like concat('%', ?, '%')";
        }
        String sql = String.format(TABLE_SQL_TEMPLATE, paramSql);
        String countSql = String.format(COUNT_SQL_TEMPLATE, sql);
        /*
            Table_catalog	数据表登记目录
            Table_schema	数据表所属的数据库名
            Table_name	表名称
            Table_type	表类型[system view|base table]
            Engine	使用的数据库引擎[MyISAM|CSV|InnoDB]
            Version	版本，默认值10
            Row_format	行格式[Compact|Dynamic|Fixed]
            Table_rows	表里所存多少行数据
            Avg_row_length	平均行长度
            Data_length	数据长度
            Max_data_length	最大数据长度
            Index_length	索引长度
            Data_free	空间碎片
            Auto_increment	做自增主键的自动增量当前值
            Create_time	表的创建时间
            Update_time	表的更新时间
            Check_time	表的检查时间
            Table_collation	表的字符校验编码集
            Checksum	校验和
            Create_options	创建选项
            Table_comment	表的注释、备注
         */
        List<Entity> query;
        int count;
        if (ObjectUtil.isNotEmpty(tableName)) {
            query = db.query(sql + PAGE_SQL_TEMPLATE, tableName, page.getStartPosition(), pageSize);
            count = db.queryNumber(countSql, tableName).intValue();
        } else {
            query = db.query(sql + PAGE_SQL_TEMPLATE, page.getStartPosition(), pageSize);
            count = db.queryNumber(countSql).intValue();
        }
        dataSource.close();
        PageData<Entity> pageData = new PageData<>();
        pageData.setCurrent(current);
        pageData.setPageSize(pageSize);
        pageData.setTotal(count);
        pageData.setList(query);
        return ResponseData.success(pageData);
    }

    @ApiOperation(value = "代码生成", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    @GetMapping(value = "/create")
    public void create(
            HttpServletResponse response
            , @ApiParam(value = "数据库驱动") @RequestParam(value = "dbDriverName", defaultValue = "com.mysql.cj.jdbc.Driver", required = false) String dbDriverName
            , @ApiParam(value = "数据库地址") @RequestParam(value = "dbUrl", defaultValue = "jdbc:mysql://127.0.0.1:3306/egg", required = false) String dbUrl
            , @ApiParam(value = "数据库用户名") @RequestParam(value = "dbUsername", defaultValue = "root", required = false) String dbUsername
            , @ApiParam(value = "数据库密码") @RequestParam(value = "dbPassword", defaultValue = "123456", required = false) String dbPassword
            , @ApiParam(value = "是否覆盖") @RequestParam(value = "fileOverride", defaultValue = "false", required = false) Boolean fileOverride
            , @ApiParam(value = "父级目录") @RequestParam(value = "parentPackage", defaultValue = "com.egg", required = false) String parentPackage
            , @ApiParam(value = "模块名称") @RequestParam(value = "moduleName", defaultValue = "basic", required = false) String moduleName
            , @ApiParam(value = "表前缀") @RequestParam(value = "tablePrefix", defaultValue = "", required = false) String tablePrefix
            , @ApiParam(value = "作者") @RequestParam(value = "author", defaultValue = "", required = false) String author
            , @ApiParam(value = "表名", required = true) @RequestParam(value = "tableName") String tableName
            , @ApiParam(value = "是否压缩") @RequestParam(value = "isZip", defaultValue = "false", required = false) Boolean isZip
    ) throws IOException {
        if (ObjectUtil.isEmpty(tableName)) {
            throw new CustomException("表名未填写");
        }
        if (null == fileOverride) {
            fileOverride = false;
        }
        if (ObjectUtil.isEmpty(author)) {
            author = System.getenv().getOrDefault("USER", "system");
        }
        DataSourceConfig dsc = buildDataSourceConfig(dbUrl, dbUsername, dbPassword, dbDriverName);
        String outputDir = buildOutputDir(isZip, moduleName);
        GlobalConfig gc = buildGlobalConfig(outputDir, fileOverride, author);
        PackageConfig pc = buildPackageConfig(tableName, tablePrefix, parentPackage, moduleName);
        TemplateConfig tc = buildTemplateConfig();
        StrategyConfig sc = buildStrategyConfig(tableName, tablePrefix);
        AutoGenerator mpg = buildAutoGenerator(dsc, gc, pc, tc, sc);
        mpg.execute();
        if (!isZip) {
            return;
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        toZip(gc.getOutputDir(), outputStream);
        byte[] data = outputStream.toByteArray();
        deleteDir(outputDir);
        response.reset();
        //切记attachment;filename=中间不能有空格，不能有\"之类转义双引号，应该是异步请求拆包获取文件名是硬编码的，
        // 如果获取不到显示默认文件名为BoostrapSwaggerUiDownload.txt，但是在原生swagger-ui中是可以解析的
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment;filename=%s.zip", tableName));
        response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(data.length));
        response.setContentType("application/octet-stream; charset=UTF-8");
        IoUtil.write(response.getOutputStream(), Boolean.TRUE, data);
    }

    /**
     * 数据源
     *
     * @param dbUrl
     * @param dbUsername
     * @param dbPassword
     * @param dbDriverName
     * @return
     */
    private DataSourceConfig buildDataSourceConfig(String dbUrl, String dbUsername, String dbPassword, String dbDriverName) {
        DataSourceConfig dataSource;
        if (ObjectUtil.isNotEmpty(dbUrl)
                && ObjectUtil.isNotEmpty(dbUsername)
                && ObjectUtil.isNotEmpty(dbPassword)
        ) {
            dataSource = new DataSourceConfig();
            dataSource.setDriverName(dbDriverName).setUrl(dbUrl).setUsername(dbUsername).setPassword(dbPassword);
        } else {
            dataSource = dataSourceConfig;
        }
        return dataSource;
    }

    /**
     * 输出目录
     *
     * @param isZip      是否压缩
     * @param moduleName 模块名称
     * @return
     */
    private String buildOutputDir(Boolean isZip, String moduleName) {
        String projectPath = System.getProperty("user.dir");
        String outputDir;
        if (isZip) {
            // 如果是下载压缩文件， 则需要删除目录
            outputDir = projectPath + StringPool.SLASH + UUID.randomUUID().toString().replace("-", "");
        } else {
            outputDir = projectPath + StringPool.SLASH + "modules" + StringPool.SLASH + moduleName + StringPool.SLASH + moduleName + "-service/src/main";
        }
        return outputDir;
    }

    /**
     * 全局配置
     *
     * @param outputDir    输出的目录
     * @param fileOverride 是否覆盖
     * @param author       作者
     * @return
     */
    private GlobalConfig buildGlobalConfig(String outputDir, Boolean fileOverride, String author) {
        GlobalConfig gc = new GlobalConfig();
        gc.setOutputDir(outputDir + "/java")
                .setFileOverride(fileOverride)
                .setAuthor(author)
                .setSwagger2(true)
                .setOpen(false)
        ;
        return gc;
    }

    /**
     * 包配置
     *
     * @param tableName     表名
     * @param tablePrefix   表前缀
     * @param parentPackage 父级目录
     * @param moduleName    模块名称
     * @return
     */
    private PackageConfig buildPackageConfig(String tableName, String tablePrefix, String parentPackage, String moduleName) {
        String packageName = tableName;
        if (ObjectUtil.isNotEmpty(tablePrefix)) {
            packageName = packageName.replace(tablePrefix + StringPool.UNDERSCORE, "");
        }
        packageName = packageName.replace(StringPool.UNDERSCORE, StringPool.DOT);
        packageName = packageName.split(StringPool.BACK_SLASH + StringPool.DOT)[0];
        PackageConfig pc = new PackageConfig();
        pc.setParent(parentPackage)
                .setModuleName(moduleName)
                .setController("controller.web")
                .setEntity("entity." + packageName)
                .setMapper("mapper." + packageName)
                .setService("service." + packageName)
                .setServiceImpl("service." + packageName + ".impl")
                .setXml("mapper." + packageName + ".xml")
        ;
        return pc;
    }

    /**
     * 配置模板
     *
     * @return
     */
    private TemplateConfig buildTemplateConfig() {
        TemplateConfig tc = new TemplateConfig();
        tc.setController("/templates/Controller.java");
        tc.setEntity("/templates/Entity.java");
        tc.setService("/templates/Service.java");
        tc.setServiceImpl("/templates/ServiceImpl.java");
        tc.setMapper("/templates/Mapper.java");
        tc.setXml("/templates/Mapper.xml");
        return tc;
    }

    /**
     * 策略配置
     *
     * @param tableName   表名
     * @param tablePrefix 表前缀
     * @return
     */
    private StrategyConfig buildStrategyConfig(String tableName, String tablePrefix) {
        List<String> superEntityFields = Arrays.asList(BaseEntity.class.getDeclaredFields()).stream()
                .map(o -> StringUtils.camelToUnderline(o.getName())).collect(Collectors.toList());
        String[] superEntityColumns = superEntityFields.toArray(new String[superEntityFields.size()]);
        StrategyConfig sc = new StrategyConfig();
        sc.setNaming(NamingStrategy.underline_to_camel)
                .setColumnNaming(NamingStrategy.underline_to_camel)
                .setEntityTableFieldAnnotationEnable(true)
                .setEntityLombokModel(true)
                .setRestControllerStyle(true)
                .setEntitySerialVersionUID(false)
                .setSuperEntityColumns(superEntityColumns)
                .setChainModel(true)
                .setSuperControllerClass(WebController.class)
                .setSuperEntityClass(BaseEntity.class)
                .setSuperServiceClass(IBaseService.class)
                .setSuperServiceImplClass(BaseServiceImpl.class)
                .setControllerMappingHyphenStyle(true)
                .setInclude(tableName);
        if (ObjectUtil.isNotEmpty(tablePrefix)) {
            sc.setTablePrefix(tablePrefix + "_");
        }
        return sc;
    }

    /**
     * 代码生成器
     *
     * @param dsc
     * @param gc
     * @param pc
     * @param tc
     * @param sc
     * @return
     */
    private AutoGenerator buildAutoGenerator(DataSourceConfig dsc, GlobalConfig gc, PackageConfig pc, TemplateConfig tc, StrategyConfig sc) {
        AutoGenerator mpg = new AutoGenerator();
        mpg.setGlobalConfig(gc);
        mpg.setDataSource(dsc);
        mpg.setPackageInfo(pc);
        mpg.setTemplate(tc);
        mpg.setStrategy(sc);
        return mpg;
    }

    /**
     * 压缩成ZIP
     *
     * @param srcDir 压缩文件夹路径
     * @param out    压缩文件输出流
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String srcDir, OutputStream out) {
        //语法糖: try块退出时，会自动调用.close()方法，关闭资源。
        try (ZipOutputStream zos = new ZipOutputStream(out)) {
            File sourceFile = new File(srcDir);
            compress(sourceFile, zos, sourceFile.getName(), false);
        } catch (Exception e) {
            log.error("压缩文件失败", e);
        }
    }

    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param keepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         <p>
     *                         false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     */
    private static void compress(
            File sourceFile
            , ZipOutputStream zos
            , String name
            , boolean keepDirStructure
    ) {
        byte[] buf = new byte[1024];
        if (sourceFile.isFile()) {
            // copy文件到zip输出流中
            int len;
            try (FileInputStream in = new FileInputStream(sourceFile)) {
                // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
                zos.putNextEntry(new ZipEntry(name));
                while ((len = in.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
                zos.closeEntry();
            } catch (IOException e) {
                log.error("写文件失败: ", e);
            }
        } else {
            File[] listFiles = sourceFile.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if (keepDirStructure) {
                    try {
                        // 空文件夹的处理
                        zos.putNextEntry(new ZipEntry(name + "/"));
                        // 没有文件，不需要文件的copy
                        zos.closeEntry();
                    } catch (IOException e) {
                        log.error("写文件失败: ", e);
                    }

                }
            } else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    if (keepDirStructure) {
                        // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
                        compress(file, zos, name + "/" + file.getName(), keepDirStructure);
                    } else {
                        compress(file, zos, file.getName(), keepDirStructure);
                    }
                }
            }
        }
    }

    /**
     * 递归删除文件
     *
     * @param path
     */
    public static void deleteDir(String path) {
        File directory = new File(path);
        //获取目录下所有文件和目录
        File files[] = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                deleteDir(file.getPath());
            } else {
                file.delete();
            }
        }
        //最终把该目录也删除
        directory.delete();
    }


}
